package jsonparser.core.lexer;

import jsonparser.exception.JSONParseException;

import java.io.IOException;

/**
 * Created by pendragon on 17-5-18.
 */
public class TokenReader {

    protected CharReader reader;

    public TokenReader(CharReader reader) {
        this.reader = reader;
    }

    public int getLine(){
        return reader.line;
    }

    public int getIndex(){
        return reader.index;
    }

    public Token readNextToken() throws IOException {
        char c;
        while (true) {
            if (!reader.hasMore())
                return Token.EOF;

            c = reader.peek(); // just look forward, not move the pointer
            if (!isWhiteSpace(c))
                break;
            else
                reader.next(); // move the pointer to next
        }

        switch (c) {
            case '{':
                reader.next();
                return Token.BEGIN_OBJECT;
            case '}':
                reader.next();
                return Token.END_OBJECT;
            case '[':
                reader.next();
                return Token.BEGIN_ARRAY;
            case ']':
                reader.next();
                return Token.END_ARRAY;
            case ':':
                reader.next();
                return Token.SEP_COLON;
            case ',':
                reader.next();
                return Token.SEP_COMMA;
            case '\"':
                return Token.STRING;
            case 'n':
                return Token.NULL;
            case 't':
            case 'f':
                return Token.BOOLEAN;
            case '-':
                return Token.NUMBER;
        }

        if (Character.isDigit(c))
            return Token.NUMBER;

        return Token.UD;
    }

    public boolean readBoolean() throws IOException {
        char c = reader.peek();
        String tem;
        if (c == 't')
            tem = "true";
        else if (c == 'f')
            tem = "false";
        else
            throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index);

        for (int i = 0; i < tem.length(); i++) {
            char next = reader.next();
            if (next != tem.charAt(i))
                throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                        + " : expected <boolean>" );
        }

        return c == 't';
    }

    /**
     * Only supports decimal integers, not supporting numbers like 0X54, 8l, 8.0f
     * -45   -45.0E0
     * 123.54   1.2354e2
     * 1.52e-4   1.52e4
     * @return number
     * @throws IOException parsing exception
     */
    public Number readNumber() throws IOException {
        final int READ_NUMBER_INT_PART = 0;
        final int READ_NUMBER_FRA_PART = 1;
        final int READ_NUMBER_EXP_PART = 2;
        final int READ_NUMBER_END = 3;

        boolean isMinus = false;
        boolean isExpMinus = false;
        char c;
        StringBuilder intPart = new StringBuilder();
        StringBuilder fracPart = new StringBuilder();
        StringBuilder expPart = new StringBuilder();

        c = reader.peek();
        if (c == '-'){
            isMinus = true;
            reader.next();
        }

        int status = READ_NUMBER_INT_PART;
        while (true){
            if (reader.hasMore())
                c = reader.peek();
            else
                status = READ_NUMBER_END;

            switch (status){
                case READ_NUMBER_INT_PART:
                    if (Character.isDigit(c)){
                        intPart.append(c);
                        reader.next();
                    } else if (c == '.'){
                        if (intPart.length() == 0){
                            throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                                    + " : expected <number>" );
                        }
                        reader.next();
                        status = READ_NUMBER_FRA_PART;
                    } else if (c == 'e' || c == 'E'){
                        reader.next();
                        status = READ_NUMBER_EXP_PART;
                    } else {
                        if (intPart.length() == 0)
                            throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                                    + " : expected <number>" );
                        else
                            status = READ_NUMBER_END;
                    }

                    continue;
                case READ_NUMBER_FRA_PART:
                    if (Character.isDigit(c)){
                        fracPart.append(c);
                        reader.next();
                    } else if (c == 'e' || c == 'E'){
                        if (fracPart.length() == 0)
                            throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                                    + " : expected <number>" );
                        else {
                            reader.next();
                            status = READ_NUMBER_EXP_PART;
                        }
                    } else {
                        if (fracPart.length() == 0)
                            throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                                    + " : expected <number>" );
                        else
                            status = READ_NUMBER_END;
                    }

                    continue;
                case READ_NUMBER_EXP_PART:
                    if (c == '-'){
                        isExpMinus = true;
                        reader.next();
                    } else if (Character.isDigit(c)){
                        expPart.append(c);
                        reader.next();
                    } else {
                        if (expPart.length() == 0)
                            throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                                    + " : expected <number>" );
                        else
                            status = READ_NUMBER_END;
                    }
                    continue;
                case READ_NUMBER_END:
                    if (intPart.length() == 0)
                        throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                                + " : expected <number>" );

                    return buildNumber(isMinus, intPart, fracPart, isExpMinus, expPart);
            }
        }
    }

    public String readString() throws IOException {
        StringBuilder sb = new StringBuilder();
        char c;

        c = reader.next();
        if (c != '"')
            throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                    + " : expected <string>" );

        while (true) {
            c = reader.next();
            // escape value
            // 1. \0 void char
            // 2. \u5b89 unicode
            // 3. \" \' \\ special char
            // 4. \r \n \t \f \b
            if (c == '\\') {
                char next = reader.next();
                switch (next) {
                    case '0':
                        break;
                    case 'u':
                        int u = 0;
                        // /u5b89
                        // refer to the Internet
                        for (int i = 0; i < 4; i++) {
                            char uc = reader.next();
                            if (uc >= '0' && uc <= '9')
                                u = (u << 4) + (uc - '0');
                            else if (uc >= 'a' && uc <= 'f')
                                u = (u << 4) + (uc - 'a') + 10;
                            else if (uc >= 'A' && uc <= 'F')
                                u = (u << 4) + (uc - 'A') + 10;
                            else
                                throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                                        + " : illegal unicode char" );
                        }
                        sb.append((char) u);
                        break;
                    case '\"':
                        sb.append('\"');
                        break;
                    case '\\':
                        sb.append('\\');
                        break;
                    case '\'':
                        sb.append('\'');
                        break;
                    case 'r':
                        sb.append('\r');
                        break;
                    case 'n':
                        sb.append('\n');
                        break;
                    case 't':
                        sb.append('\t');
                        break;
                    case 'f':
                        sb.append('\f');
                        break;
                    case 'b':
                        sb.append('\b');
                        break;
                    default:
                        throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                                + " : illegal escape charsequence" );
                }
            } else if (c == '\"'){
                break;
                //todo 是否还有其他情况
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    public void readNull() throws IOException {
        String tem = "null";
        for (int i = 0; i < tem.length(); i++) {
            char next = reader.next();
            if (next != tem.charAt(i))
                throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                + " : expected <null>" );
        }
    }

    private boolean isWhiteSpace(char c) {
        return c == ' ' || c == '\t' || c == '\n' || c == '\r';
    }

    private double buildNumber(boolean isMinus, StringBuilder intPart, StringBuilder fracPart,
                               boolean isExpMinus, StringBuilder expPart){
        try {
            double num = Integer.parseInt(intPart.toString());

            if (fracPart.length() != 0){
                num = num + Integer.parseInt(fracPart.toString()) / Math.pow(10, fracPart.length());
            }

            if (expPart.length() != 0){
                if (num < 1 || num >= 10)
                    throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                            + " : illegal scientific format");

                if (isExpMinus)
                    num = num * Math.pow(10, 0 - Integer.parseInt(expPart.toString()));
                else
                    num = num * Math.pow(10, Integer.parseInt(expPart.toString()));
            }

            return isMinus ? 0 - num : num;
        } catch (NumberFormatException e){
            throw new JSONParseException("Error: line " + reader.line + ", at index " + reader.index
                    + " : " + e.getMessage());
        }
    }

}
